{
"cells": [
{
"cell_type": "markdown",
"id": "cell-0",
"metadata": {},
"source": "# Coordinate Frames\n\nSatellite astrodynamics requires working in several coordinate reference frames, each designed for a different purpose. This tutorial describes the frames `satkit` supports and how to rotate vectors between them.\n\n> This tutorial is about the **frames themselves** — their definitions and the rotations between them. For working with a specific *point* on Earth's surface (geodetic lat / lon / altitude, local ENU / NED tangent planes, geodesic distances), see the **Geodetic Coordinates** tutorial, which describes the `itrfcoord` data type.\n\nThe three most important frames for most users are:\n\n- **GCRF** (Geocentric Celestial Reference Frame) — An Earth-centered inertial frame. Equations of motion are simplest in this frame because it does not rotate with the Earth. Used for orbit propagation, conjunction analysis, and any dynamics computation.\n\n- **ITRF** (International Terrestrial Reference Frame) — An Earth-centered, Earth-fixed frame that rotates with the Earth. Ground station positions, GPS coordinates, and geodetic quantities (latitude, longitude, altitude) are expressed in this frame.\n\n- **TEME** (True Equator, Mean Equinox) — A quasi-inertial frame that is the native output of the SGP4 orbit propagator used with Two-Line Element sets (TLEs). It accounts for precession and a simplified nutation model but not the full IERS reduction. Positions from SGP4 must be rotated out of TEME before they can be compared with GCRF or ITRF data.\n\n`satkit` also exposes several intermediate and satellite-local frames used internally by the reduction chain and by the propagator:\n\n- **CIRS / TIRS** — the Celestial and Terrestrial Intermediate Reference Systems, the two steps of the IERS 2010 reduction chain between GCRF and ITRF.\n- **EME2000 / ICRF** — J2000 inertial and the International Celestial Reference Frame, which differ from GCRF at the milliarcsecond level.\n- **RTN / NTW / LVLH** — satellite-local frames used for maneuvers, covariance, and relative motion. See the **Theory: Maneuver Coordinate Frames** guide for a comparison.\n\n## How are GCRF and ITRF actually defined?\n\nGCRF and ITRF are **realizations** of the *International Celestial Reference System* (ICRS) and the *International Terrestrial Reference System* (ITRS) respectively. A reference *system* is a mathematical definition (origin, axis orientation, time scale); a reference *frame* is a concrete realization built from a set of physically-measured fiducial points. The distinction matters because the frames drift slightly over time as new measurements refine the positions of the fiducial points.\n\n### GCRF / GCRS — fixed to distant quasars\n\nThe **GCRS** (Geocentric Celestial Reference System) is a non-rotating, kinematically non-rotating, Earth-centered inertial system. Its axes are fixed with respect to **extragalactic radio sources** — mostly distant quasars — whose positions are measured by **Very Long Baseline Interferometry (VLBI)** using a globally distributed network of radio telescopes.\n\nThe practical realization is the **ICRF** (International Celestial Reference Frame), currently ICRF3 (adopted by the IAU in 2018), which is a catalog of ~4500 radio-source positions measured to milliarcsecond accuracy. GCRS/GCRF axes are aligned with these quasars — effectively \"fixed stars at infinity\" — and because quasars are billions of light-years away, they exhibit no detectable proper motion over human timescales. This gives GCRF the closest thing to a truly inertial reference that exists.\n\nThe GCRS origin is the **geocenter** (Earth's center of mass), with the standard relativistic metric of general relativity in effect. The \"geocentric\" qualifier distinguishes it from the **BCRF** (Barycentric Celestial Reference Frame) whose origin is the solar system barycenter.\n\n### ITRF / ITRS — fixed to Earth's crust\n\nThe **ITRS** (International Terrestrial Reference System) is a rotating, Earth-centered, Earth-fixed system. Its axes are fixed to points *on the Earth's surface* — specifically, to a globally distributed network of **geodetic tracking stations** whose positions and velocities are continuously measured by four space-geodetic techniques:\n\n- **GNSS** (GPS, GLONASS, Galileo, BeiDou) — ~500 stations\n- **VLBI** (the same radio telescopes that define ICRF) — several dozen\n- **SLR** (Satellite Laser Ranging) — a few dozen\n- **DORIS** (Doppler orbit determination from a French radio-beacon network) — ~60 stations\n\nThe **ITRF** (the realization of ITRS) is computed by the IERS by combining observations from all four techniques, solving for station positions and velocities that best fit the data. Successive realizations (ITRF2000, ITRF2008, ITRF2014, ITRF2020, ...) refine the frame as more data accumulates. Station velocities are included because continental plates move at centimeters per year, so the \"Earth-fixed\" frame is only fixed on the scale of decades.\n\nKey points:\n\n- **Origin**: Earth's center of mass (geocenter), determined from satellite tracking.\n- **Orientation**: Aligned with Earth's crust such that the time-averaged \"horizontal\" tectonic motion over the whole globe is zero (the \"no-net-rotation\" condition).\n- **Z-axis**: The IERS Reference Pole, close to the mean Earth rotation axis (polar motion offsets are at the 0.3 arcsecond level).\n- **X-axis**: Through the IERS Reference Meridian, close to the historical Greenwich meridian (but ~5 m off, because the IRM is defined from VLBI/GPS-computed station positions, not the brass strip at Greenwich).\n\n### Why two frames? Why the transform matters\n\nThe Earth rotates approximately once per sidereal day relative to GCRF, so a point fixed in ITRF (say, a GPS station) traces out a nearly-circular path in GCRF. Converting between the two requires accounting for:\n\n1. **Polar motion** — the ~10 meter wobble of the Earth rotation axis relative to the crust (Chandler wobble, annual term, drift)\n2. **Earth rotation** — the ~15°/hr daily rotation, parameterized by the Earth Rotation Angle (ERA) or GAST, with small corrections from **UT1−UTC** (the Earth rotates at slightly non-constant speed due to atmospheric angular momentum, ocean tides, etc.)\n3. **Precession and nutation** — the ~26,000-year wobble of Earth's rotation axis due to Sun/Moon torques, plus ~18.6-year and smaller nutation terms\n\nAll three effects are measured continuously by IERS and published as **Earth Orientation Parameters** (EOP): polar motion (xp, yp), UT1−UTC, and nutation corrections (dX, dY). satkit downloads EOP automatically via `sk.utils.update_datafiles()` and applies them in the full ITRF↔GCRF transforms.\n\n## The quaternion API\n\nAll frame rotations in satkit are represented as unit quaternions. Applying a rotation to a 3-vector is done with the `*` operator: `v_out = q * v_in`.\n\nThe rest of this tutorial demonstrates the rotation functions in `satkit.frametransform`."
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-1",
"metadata": {
"execution": {
"iopub.execute_input": "2026-04-05T17:39:13.780992Z",
"iopub.status.busy": "2026-04-05T17:39:13.780616Z",
"iopub.status.idle": "2026-04-05T17:39:13.973383Z",
"shell.execute_reply": "2026-04-05T17:39:13.973122Z"
}
},
"outputs": [],
"source": [
"import ssl, certifi\n",
"# Point urllib at certifi's CA bundle so cartopy can download Natural Earth\n",
"# shapefiles over HTTPS without hitting macOS's stricter cert store.\n",
"ssl._create_default_https_context = lambda: ssl.create_default_context(cafile=certifi.where())\n",
"\n",
"import satkit as sk\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import scienceplots # noqa: F401\n",
"plt.style.use([\"science\", \"no-latex\", \"../satkit.mplstyle\"])\n",
"%config InlineBackend.figure_formats = ['svg']\n",
"\n",
"import warnings\n",
"warnings.filterwarnings(\"ignore\", \"Downloading\")"
]
},
{
"cell_type": "markdown",
"id": "cell-2",
"metadata": {},
"source": "## Basic Frame Rotations\n\nThe `satkit.frametransform` module provides quaternion rotation functions between the major frames. Each function takes a time (or array of times) and returns the corresponding quaternion(s).\n\n| Function | Rotation |\n|---|---|\n| `qitrf2gcrf(t)` | ITRF -> GCRF (full IERS 2010) |\n| `qgcrf2itrf(t)` | GCRF -> ITRF (full IERS 2010) |\n| `qitrf2gcrf_approx(t)` | ITRF -> GCRF (approx., ~1 arcsec) |\n| `qgcrf2itrf_approx(t)` | GCRF -> ITRF (approx., ~1 arcsec) |\n| `qteme2gcrf(t)` | TEME -> GCRF |\n| `qteme2itrf(t)` | TEME -> ITRF |\n\nLet's start with a concrete example: rotating a ground station's ITRF position into the GCRF inertial frame."
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-3",
"metadata": {
"execution": {
"iopub.execute_input": "2026-04-05T17:39:13.974660Z",
"iopub.status.busy": "2026-04-05T17:39:13.974566Z",
"iopub.status.idle": "2026-04-05T17:39:13.984992Z",
"shell.execute_reply": "2026-04-05T17:39:13.984750Z"
}
},
"outputs": [],
"source": [
"# Define a ground station using geodetic coordinates\n",
"station = sk.itrfcoord(latitude_deg=42.0, longitude_deg=-71.0, altitude=100)\n",
"print(f'Station ITRF position: {station}')\n",
"print(f'ITRF Cartesian vector (m): {station.vector}')\n",
"\n",
"# Pick a specific time\n",
"t = sk.time(2024, 6, 15, 12, 0, 0)\n",
"\n",
"# Get the ITRF -> GCRF rotation quaternion at this time\n",
"q = sk.frametransform.qitrf2gcrf(t)\n",
"\n",
"# Rotate the ITRF position vector to GCRF\n",
"pos_gcrf = q * station.vector\n",
"print(f'\\nGCRF position at {t}: {pos_gcrf}')\n",
"print(f'Position magnitude: {np.linalg.norm(pos_gcrf):.1f} m (unchanged by rotation)')\n"
]
},
{
"cell_type": "markdown",
"id": "cell-4",
"metadata": {},
"source": [
"Note that the magnitude of the position vector is preserved -- quaternion rotations are rigid-body rotations.\n",
"\n",
"The inverse rotation (GCRF back to ITRF) should recover the original vector:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-5",
"metadata": {
"execution": {
"iopub.execute_input": "2026-04-05T17:39:14.000189Z",
"iopub.status.busy": "2026-04-05T17:39:14.000095Z",
"iopub.status.idle": "2026-04-05T17:39:14.002068Z",
"shell.execute_reply": "2026-04-05T17:39:14.001870Z"
}
},
"outputs": [],
"source": [
"# Rotate back from GCRF to ITRF\n",
"q_inv = sk.frametransform.qgcrf2itrf(t)\n",
"pos_roundtrip = q_inv * pos_gcrf\n",
"\n",
"print(f'Original ITRF vector: {station.vector}')\n",
"print(f'Round-trip ITRF vector: {pos_roundtrip}')\n",
"print(f'Difference (m): {np.linalg.norm(pos_roundtrip - station.vector):.2e}')\n"
]
},
{
"cell_type": "markdown",
"id": "cell-6",
"metadata": {},
"source": "## Approximate vs Full IERS 2010 Reduction\n\nThe full IERS 2010 ITRF-GCRF rotation accounts for precession, nutation, Earth rotation angle, and polar motion using the complete IERS conventions. This is computationally expensive.\n\nThe `_approx` variants use a simplified model that omits the small high-frequency nutation terms and polar motion. The plot below shows the resulting angular error over several decades.\n"
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-7",
"metadata": {
"execution": {
"iopub.execute_input": "2026-04-05T17:39:14.003052Z",
"iopub.status.busy": "2026-04-05T17:39:14.002995Z",
"iopub.status.idle": "2026-04-05T17:39:14.256451Z",
"shell.execute_reply": "2026-04-05T17:39:14.256230Z"
}
},
"outputs": [],
"source": [
"# Compare full and approximate ITRF -> GCRF rotation over a span of years\n",
"import math as m\n",
"\n",
"start = sk.time(2000, 1, 1)\n",
"end = sk.time(2030, 1, 1)\n",
"duration = end - start\n",
"timearray = np.array([start + sk.duration.from_days(x)\n",
" for x in np.linspace(0, duration.days, 4000)])\n",
"\n",
"qexact = sk.frametransform.qgcrf2itrf(timearray)\n",
"qapprox = sk.frametransform.qgcrf2itrf_approx(timearray)\n",
"qdiff = np.array([q1 * q2.conjugate for q1, q2 in zip(qexact, qapprox)])\n",
"theta_arcsec = np.array([min(q.angle, 2 * m.pi - q.angle) for q in qdiff]) * 180.0 / m.pi * 3600\n",
"\n",
"fig, ax = plt.subplots(figsize=(8, 4))\n",
"ax.plot([t.as_datetime() for t in timearray], theta_arcsec, \"k-\", linewidth=1)\n",
"ax.set_xlabel(\"Year\")\n",
"ax.set_ylabel(\"Angular error [arcsec]\")\n",
"ax.set_title(\"Approximate vs Full ITRF to GCRF Rotation\")\n",
"ax.grid(True, alpha=0.3)\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(f'Peak error over {start.as_datetime().year}-{end.as_datetime().year}: '\n",
" f'{theta_arcsec.max():.3f} arcsec')\n"
]
},
{
"cell_type": "markdown",
"id": "cell-8",
"metadata": {},
"source": [
"The error stays at the arcsecond level, which is more than sufficient for ground-track plotting, pass prediction, and other applications that do not require sub-arcsecond accuracy. For high-precision orbit determination, use the full reduction.\n"
]
},
{
"cell_type": "markdown",
"id": "cell-9",
"metadata": {},
"source": "## TEME: the SGP4 output frame\n\nThe SGP4 propagator outputs position and velocity in **TEME** (True Equator, Mean Equinox of date). TEME is not part of the IERS 2010 conventions — it is a historical frame specific to the SGP4 mathematical model — but because SGP4 + TLEs are the most widely distributed source of satellite ephemerides, every astrodynamics library has to handle it.\n\n### Where the name comes from\n\n\"True Equator, Mean Equinox\" describes a deliberate half-and-half construction:\n\n- **True equator of date** — the xy-plane is the Earth's *true* (instantaneous) equatorial plane at the epoch, accounting for both precession *and* nutation. The z-axis is the true celestial pole at that instant.\n- **Mean equinox of date** — the x-axis points towards the *mean* vernal equinox, i.e., the intersection of the mean ecliptic with the mean equator, including precession but **without** the nutation-in-longitude correction that would shift it to the true equinox.\n\nThe mismatch between \"true equator\" and \"mean equinox\" is intentional. SGP4 was developed at NORAD in the 1960s-70s when computing the full nutation series on-orbit was expensive; TEME pins the pole with the full nutation model but skirts the more expensive nutation-in-longitude term for the equinox. The residual rotation between TEME and the true-equator true-equinox frame (TOD) is a single small rotation about the z-axis — the **equation of the equinoxes** — which is a well-known small quantity (bounded by about ±1.2 arcseconds).\n\n### Why TEME is awkward\n\nThree practical points about TEME:\n\n- **It is not uniquely defined.** Vallado, Crawford, Hujsak & Kelso's \"Revisiting Spacetrack Report #3\" (AIAA 2006) showed that multiple slightly different \"TEME\" conventions were in use — differing by how the equation of the equinoxes was computed and by which nutation series was applied. Older Spacetrack Report #3 code uses a truncated 1980 IAU nutation series; modern SGP4 implementations use a consistent variant. `satkit` follows Vallado's reference formulation.\n- **TLE epoch time is UTC, but TEME is a quasi-inertial frame of that instant.** The frame's orientation is time-dependent because precession and nutation move the pole and equinox. Rotations from TEME must be evaluated *at the TLE epoch* (or the propagation time, for sub-epoch queries), not at a fixed reference date.\n- **TEME positions cannot be compared directly to anything.** TLE-derived positions must be rotated to GCRF (for inertial comparisons, catalog cross-match, conjunction analysis) or to ITRF (for ground tracks, visibility, geodetic sub-satellite points) before use. Error from skipping this step is typically ~km-level — small enough to look plausible, large enough to corrupt any quantitative comparison.\n\n### The `satkit` API\n\n`satkit` provides two direct TEME rotations:\n\n| Function | Rotation | Notes |\n|---|---|---|\n| `qteme2gcrf(t)` | TEME → GCRF | Applies precession + nutation + frame bias to recover the inertial GCRF frame |\n| `qteme2itrf(t)` | TEME → ITRF | Composes the above with the ITRF rotation (includes Earth rotation and polar motion) |\n\nBoth accept scalar times or arrays. The inverse rotations are available via the quaternion conjugate (`q.conjugate`).\n\n### Reference\n\n- Vallado, D. A., Crawford, P., Hujsak, R., and Kelso, T. S. \"Revisiting Spacetrack Report #3,\" AIAA 2006-6753 — the canonical treatment of SGP4 and TEME.\n\nBelow, we propagate the ISS using SGP4 and convert the results to both GCRF and ITRF."
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-10",
"metadata": {
"execution": {
"iopub.execute_input": "2026-04-05T17:39:14.257753Z",
"iopub.status.busy": "2026-04-05T17:39:14.257675Z",
"iopub.status.idle": "2026-04-05T17:39:14.259580Z",
"shell.execute_reply": "2026-04-05T17:39:14.259345Z"
}
},
"outputs": [],
"source": [
"# ISS TLE (example epoch)\n",
"line1 = '1 25544U 98067A 24167.50000000 .00016717 00000-0 10270-3 0 9002'\n",
"line2 = '2 25544 51.6400 200.0000 0001000 90.0000 270.0000 15.49000000400000'\n",
"tle = sk.TLE.from_lines([line1, line2])\n",
"print(f'TLE epoch: {tle.epoch}')\n",
"\n",
"# Propagate at a single time near epoch\n",
"t = tle.epoch\n",
"pos_teme, vel_teme = sk.sgp4(tle, t)\n",
"print(f'\\nTEME position (m): {pos_teme}')\n",
"print(f'TEME velocity (m/s): {vel_teme}')\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-11",
"metadata": {
"execution": {
"iopub.execute_input": "2026-04-05T17:39:14.260509Z",
"iopub.status.busy": "2026-04-05T17:39:14.260449Z",
"iopub.status.idle": "2026-04-05T17:39:14.262880Z",
"shell.execute_reply": "2026-04-05T17:39:14.262696Z"
}
},
"outputs": [],
"source": [
"# Rotate TEME position and velocity to GCRF\n",
"q_teme2gcrf = sk.frametransform.qteme2gcrf(t)\n",
"pos_gcrf = q_teme2gcrf * pos_teme\n",
"vel_gcrf = q_teme2gcrf * vel_teme\n",
"print(f'GCRF position (m): {pos_gcrf}')\n",
"print(f'GCRF velocity (m/s): {vel_gcrf}')\n",
"\n",
"# Rotate TEME position to ITRF. At this point we have a Cartesian\n",
"# (X, Y, Z) vector in the Earth-fixed frame — the three numbers describe\n",
"# where the satellite is relative to Earth's centre, with +X through the\n",
"# Greenwich meridian and +Z along the rotation axis.\n",
"q_teme2itrf = sk.frametransform.qteme2itrf(t)\n",
"pos_itrf = q_teme2itrf * pos_teme\n",
"print(f'\\nITRF Cartesian (m): {pos_itrf}')\n",
"\n",
"# `sk.itrfcoord` wraps the ITRF Cartesian vector and exposes geodetic\n",
"# coordinates. The distinction matters:\n",
"#\n",
"# * Cartesian — (X, Y, Z) in meters, Earth-fixed. What the rotation\n",
"# actually gives us. Simple and unambiguous but not how\n",
"# humans think about \"where on Earth\".\n",
"#\n",
"# * Geodetic — (latitude, longitude, altitude) on the WGS-84\n",
"# reference ellipsoid. Latitude is the angle between\n",
"# the *ellipsoid normal* and the equatorial plane\n",
"# (NOT the angle from the centre, which would be\n",
"# \"geocentric latitude\"). Altitude is the perpendicular\n",
"# distance above the ellipsoid surface. This is the\n",
"# convention used by GPS, almost all maps, and by\n",
"# aviation / surveying standards.\n",
"#\n",
"# `sk.itrfcoord(pos_itrf)` auto-detects that it was given a 3-vector and\n",
"# converts Cartesian → geodetic using the WGS-84 ellipsoid parameters.\n",
"subsatpoint = sk.itrfcoord(pos_itrf)\n",
"print(f'Sub-satellite point: {subsatpoint}')\n",
"print(f' latitude: {subsatpoint.latitude_deg:+8.4f}° (geodetic, WGS-84)')\n",
"print(f' longitude: {subsatpoint.longitude_deg:+8.4f}°')\n",
"print(f' altitude: {subsatpoint.altitude / 1e3:.2f} km above WGS-84 ellipsoid')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-12",
"metadata": {
"execution": {
"iopub.execute_input": "2026-04-05T17:39:14.263769Z",
"iopub.status.busy": "2026-04-05T17:39:14.263708Z",
"iopub.status.idle": "2026-04-05T17:39:14.497621Z",
"shell.execute_reply": "2026-04-05T17:39:14.497362Z"
}
},
"outputs": [],
"source": [
"# Propagate over one full orbit and plot the ground track\n",
"import cartopy.crs as ccrs\n",
"import cartopy.feature as cfeature\n",
"\n",
"period_minutes = 1440 / tle.mean_motion # period in minutes\n",
"prop_minutes = np.linspace(0, period_minutes, 500)\n",
"prop_times = [tle.epoch + sk.duration.from_minutes(float(m)) for m in prop_minutes]\n",
"\n",
"# TEME (SGP4 output) -> ITRF Cartesian -> WGS-84 geodetic (lat, lon)\n",
"lats = []\n",
"lons = []\n",
"for t in prop_times:\n",
" pos_teme, _ = sk.sgp4(tle, t)\n",
" pos_itrf = sk.frametransform.qteme2itrf(t) * pos_teme\n",
" coord = sk.itrfcoord(pos_itrf) # Cartesian -> geodetic via WGS-84\n",
" lats.append(coord.latitude_deg)\n",
" lons.append(coord.longitude_deg)\n",
"\n",
"lon_arr = np.array(lons)\n",
"lat_arr = np.array(lats)\n",
"\n",
"# Break the ground track at date-line crossings so matplotlib doesn't\n",
"# draw a long horizontal line across the map when the satellite wraps\n",
"# from +180 deg to -180 deg.\n",
"breaks = np.where(np.abs(np.diff(lon_arr)) > 180)[0] + 1\n",
"lon_segs = np.split(lon_arr, breaks)\n",
"lat_segs = np.split(lat_arr, breaks)\n",
"\n",
"fig, ax = plt.subplots(figsize=(11, 5.5),\n",
" subplot_kw={\"projection\": ccrs.PlateCarree()})\n",
"ax.add_feature(cfeature.LAND, facecolor=\"lightgray\")\n",
"ax.add_feature(cfeature.OCEAN, facecolor=\"#dfefff\")\n",
"ax.add_feature(cfeature.COASTLINE, linewidth=0.5)\n",
"ax.add_feature(cfeature.BORDERS, linewidth=0.4, linestyle=\":\")\n",
"ax.gridlines(draw_labels=True, linewidth=0.3, alpha=0.5, linestyle=\"--\")\n",
"\n",
"for lo, la in zip(lon_segs, lat_segs):\n",
" ax.plot(lo, la, linewidth=1.5, color=\"C0\", transform=ccrs.PlateCarree())\n",
"\n",
"# Mark start with a filled circle\n",
"ax.plot(lon_arr[0], lat_arr[0], \"o\", color=\"C3\", markersize=8,\n",
" transform=ccrs.PlateCarree(), label=\"Start\")\n",
"ax.set_title(\"ISS Ground Track, One Orbit (TEME to ITRF to WGS-84 geodetic)\")\n",
"ax.set_global()\n",
"ax.legend(loc=\"lower left\")\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "cell-13",
"metadata": {},
"source": "## Summary\n\nThe `satkit.frametransform` module provides all the rotations needed to convert between the standard coordinate frames used in satellite astrodynamics:\n\n- Use `qitrf2gcrf` / `qgcrf2itrf` for converting between Earth-fixed and inertial frames (full IERS 2010 accuracy)\n- Use the `_approx` variants when sub-arcsecond accuracy is not required\n- Use `qteme2gcrf` and `qteme2itrf` to convert SGP4/TLE outputs to standard frames\n- All functions accept both scalar times and arrays, returning quaternion(s) accordingly\n- Apply rotations to 3-vectors with the `*` operator: `v_out = q * v_in`"
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}